Skip to content

fix(mcp): account-sync resilience — bound account-map cache + surface status#22

Merged
hamr0 merged 1 commit into
masterfrom
fix/account-sync-resilience
Jun 20, 2026
Merged

fix(mcp): account-sync resilience — bound account-map cache + surface status#22
hamr0 merged 1 commit into
masterfrom
fix/account-sync-resilience

Conversation

@hamr0

@hamr0 hamr0 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Fixes the account-sync wedge filed by multis (docs/01-product/beeperbox-asks/account-sync-resilience.md): a WhatsApp account added via noVNC didn't surface, and a plain docker restart didn't recover it — it self-corrected only later.

Root cause (debug-method, evidence-grounded)

getAccountMap() — the accountID → network map every chat verb uses for network labels — cached its result for the whole process lifetime with no TTL and no invalidation (if (accountCache) return accountCache;), and happily froze an empty map captured during the backend's post-restart sync window (when /v1/accounts briefly returns []).

Consequences, all matching the report:

  • A runtime account-add rendered network:"unknown" in every chat verb until the MCP process restarted.
  • A cold cache populated during the sync window froze empty for the whole run → a plain docker restart re-poisoned it from a still-syncing backend → wedge survived, recovering only on a luckier restart / full down+up.

list_accounts itself is stateless (live read), so its 0 was the backend's transient empty during sync — relayed silently, indistinguishable from "no accounts." The backend's per-account status field was dropped entirely.

Fix

  • Bound the cache: TTL BEEPERBOX_ACCOUNT_CACHE_TTL_MS (default 60s; 0 = always live) and never cache an empty result — served for the one call that saw it, re-read next call, so the map self-heals the instant Beeper finishes syncing. Runtime account-add now reflected within the TTL, no restart.
  • Surface status on list_accounts (connected/connecting/… — new schema field) so a syncing bridge ≠ a missing account.
  • Observability: stderr WARN when /v1/accounts returns 0, instead of silently relaying an ambiguous empty list.

Verification

  • 4 new unit tests, each written to fail against the old unbounded cache (watched fail → 3 fail/1 guard, then implemented → all pass). 48/48 green.
  • Live-validated against the running container: list_accounts returns status for all 5 accounts (WhatsApp incl.); list_inbox resolves 0 unknown-network chats.
  • Docs updated: beeperbox.context.md schema + retry guidance, docs/PRD.md env table.

Acceptance (from the ask)

  • ✅ backend healthy with N accounts → list_accounts returns N (live read).
  • ✅ add/remove account reflected without a full teardown (within TTL; list_accounts instant).
  • ✅ divergence is logged, not silent.
  • ✅ a docker restart recovers a wedged relay (empty is never frozen).

Healthcheck left unchanged on purpose — failing it on 0 accounts would wedge first-run/fresh-login containers (legitimately 0) and the release gate; the stderr WARN is the observability path instead.

Release scope: MINOR (new status schema field) when cut.

🤖 Generated with Claude Code

…lience)

Root cause of the multis-reported wedge (WhatsApp added via noVNC didn't
surface; a plain `docker restart` didn't recover it): getAccountMap() cached
the accountID->network map for the whole process lifetime with no TTL/
invalidation, and froze an EMPTY map captured during the backend's post-
restart sync window. So a runtime account-add stayed network:"unknown" until
the process restarted, and a cold cache re-poisoned itself from a still-
syncing backend on the next restart.

- getAccountMap: TTL-bound the cache (BEEPERBOX_ACCOUNT_CACHE_TTL_MS, default
  60s; 0 = always live) and NEVER cache an empty result — serve it for the one
  call, re-read next call, so the map self-heals when Beeper finishes syncing.
- list_accounts: surface the backend per-account `status` (connected/
  connecting/…) so a syncing bridge != a missing account; new schema field.
- Observability: warn to stderr when /v1/accounts returns 0 (list_accounts +
  account-map refresh) instead of silently relaying an ambiguous 0.
- Tests: 4 new unit tests (empty-not-cached, within-TTL caching, runtime add
  visible after TTL without restart, status surfaced); each written to fail
  against the old unbounded cache. 48/48 green.
- Live-validated against the running container: list_accounts now returns
  status for all 5 accounts; list_inbox resolves 0 unknown-network chats.
- Docs: context.md schema + retry guidance, PRD env table.

Healthcheck left unchanged on purpose: failing it on 0 accounts would wedge
first-run/fresh-login containers (legitimately 0) and the release gate; the
stderr WARN is the observability path instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hamr0 hamr0 merged commit d96b279 into master Jun 20, 2026
2 checks passed
@hamr0 hamr0 deleted the fix/account-sync-resilience branch June 20, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant